1. Trabajo colaborativo

El flujo de trabajo que hasta ahora hemos visto es:

    > git init                  # inicializa un repositorio
    > git add <archivo>         # da a conocer a git un archivo nuevo o un cambio en un archivo
    > git status                # resume los cambios actuales
    > git commit -m "mensaje"   # saca una foto instantánea del estado actual del proyecto
    > git push                  # sube los cambios a un repositorio central (local o remoto)
    > git log                   # muestra la bitácora del proyecto

(Por ahora ">" indicará la línea de comandos.)

Ahora veremos un modelo posible de colaboración, que iremos complicando poco a poco.

Para empezar, la instrucción que sirve para hacer una copia local de un repositorio remoto es:

> git clone https://github.com/Usuario/proyecto.git

En la instrucción de arriba, hemos considerado un proyecto remoto que está en GitHub, por ejemplo. Sin embargo, el comando puede usar en otras situaciones, por ejemplo, un proyecto remoto en una máquina a la que tenemos acceso con ssh, o git, o un proyecto en otro directorio.

Entre otras cosas que quedan configuradas cuando uno hace la clonación de un proyecto, es dónde se encuentra el proyecto original (origin). Para verificar esto usamos:

    > git remote -v

La situación que consideraremos es la siguiente: Alicia (Alice) y Beto (Bob) colaboran en un proyecto (el que acabamos de clonar). Ambos tienen la misma versión del código.

Alicia: Alicia edita el archivo archivo.txt, y hace algún cambio que le parece conveniente. Siguiendo el esquema de trabajo que describimos arriba, Alicia sube los cambios a su repositorio local con git add y git commit, y finalmente los sube al repositorio central: git push.

Beto: Beto, por su parte y de manera independiente, hace cambios al mismo archivo en que trabajó Alicia. De la misma manera que lo hizo Alicia, Beto actualiza su repositorio local (git add y git commit) y los sube al repositorio que comparten con git push.

Sin embargo, como él editó el mismo archivo en el que Alicia hizo cambios, pero usando una versión atrasada que no incluye los cambios de Alicia, entonces git detecta que hubo cambios divergentes entre la versión local de Beto, en la rama master, y la del repositorio remoto origin/master. Esto hace que git no permita subir los cambios que propone Beto, hasta que Beto resuelva los conflictos que hayan surgido.


Ejercicio 1: Trabajando en grupos de dos al menos, traten de reproducir la situación descrita arriba. La pregunta concreta es qué significa eso del conflicto que se debe resolver.

  1. Para llevar esto a cabo, una posibilidad es a partir de lo que hicimos la vez pasada. Sin embargo, hay ciertas sutilezas que tienen que ver con que git init por default crea lo que se llama un repositorio non-bare (no vacío), y git no permite subir los cambios a un repositorio no vacío. Suponiendo que el repositorio que creamos anteriormente está en ~/Documentos/claseLuisDavid, entonces:

    (a) Primero crearemos un proyecto bare (vacío) a partir del anterior:

         > git clone --bare -l ~/Documentos/claseLuisDavid repo_vacio

    (Si inspeccionan el repositorio repo_vacio verán que contiene los archivos que normalmente se encuentran en el directorio escondido ".git", y nada más, o sea, no tiene los archivos propios del proyecto.)

    (b) A partir de este repositorio vacío, clonaremos a dos directorios independientes (Alicia/ y Beto/):

         > git clone repo_vacio Alicia
         > git clone repo_vacio Beto

    (c) Desde el directorio Alicia/ hagan un cambio importante y súbanlo al repo (git push); traten ahora de hacer lo mismo desde Beto/

  2. La segunda opción es clonar dos veces el repo que subieron a GitHub, a dos máquinas distintas o a dos directorios distintos. En este caso hay que adecuar la instrucción (b), usando la dirección del repo en GitHub. Otra sutileza es que el proyecto que clonan les debe permitir subir los cambios a ambos usuarios (en el caso de que hayan clonado a cuentas distintas o a máquinas distintas). Esto se puede configurar en los Settings del repo en GitHub; para que esto funcione ambos usuarios deben estar dados de alta en GitHub.

(A la larga, la segunda opción es más útil que la primera.)


Para entender el problema, ejecutaremos la instrucción

> git log --oneline

lo que muestra el último cambio que hizo Beto. Por otro lado, para ver cómo está el repositorio remoto hacemos:

> git log --oneline origin/master

La segunda instrucción no muestra los cambios de Alicia. Esto muestra que el repositorio de Beto no está actualizado respecto al repositorio central.

La manera en que Beto debe resolver el conflicto es, pues, actualizando su versión local respecto al repositorio remoto. Esto se hace usando:

> git fetch origin

y para ver el estado del repositorio

> git log --oneline origin/master

Entonces, para resolver el problema, Beto ha de implementar los cambios del repositorio remoto en su repositorio primero, ya que el de referencia es siempre el remoto (rama master):

> git merge origin/master

lo que hace manifiesto, nuevamente, el conflicto.


Ejercicio 2: Edita el archivo con conflictos, y resuélvelos. Después haz git add y git commit. ¿Puedes subir (push) los cambios al repositorio central?


La moraleja de esto es: antes de hacer cualquier cambio, hay que mantenerse actualizado respecto al repositorio central. Esto se hace con la combinación git fetch y git merge cuando sea necesario, en particular, antes de subir algún cambio.

Una manera "corta" y combinada de hacer los dos pasos arriba descritos (git fetch y git merge) es con el comando:

> git pull

Ejercicio 3: Actualicen el directorio Alicia/ respecto a los últimos cambios hechos por Beto.


2. Trabajando en ramas

El concepto de una rama ("branch") en git provee una forma sencilla y eficiente de trabajar en nuevas ideas, o de colaborar en un proyecto común, evitando romper cosas que a priori ya funcionan.

Para empezar, listemos las ramas existentes de un proyecto (por ejemplo, en el directorio Alicia/):

    > git branch

o usando

    > git branch -v

que brinda además el hash del último commit. Lo que esto indica es que existe únicamente la rama master, que es la rama que se crea por default (y en algún sentido es la principal), y el asterisco indica que estamos trabajando en esa rama.

Para crear una nueva rama, ejecutamos:

    > git branch <nombre_rama>

donde <nombre_rama> es el nombre de la rama, que es más o menos arbitrario y flexible. Un ejemplo es: git branch alicia, que es el que usaré en este ejemplo; otra posibilidad podría ser git branch alicia/nuevaidea.

Después de ejecutar alguna de estas instrucciones, git branch -v nos informa que ambas ramas, master y alicia existen, ambas están en el (mismo) último commit, y el asterisco indica que estamos en la rama master aún.

Para cambiarnos de rama, ejecutamos:

    > git checkout <nombre_rama>

Nuevamente, existe un atajo para crear y cambiarnos de rama de un golpe: git checkout -b <nombre_rama>.


Ejercicio 4: (a) Creen una rama, cámbiense a la nueva rama, y verifiquen que están en la nueva rama. (b) Pregunta: la instrucción git status, ¿da alguna información sobre en qué rama están?

Ejercicio 5: Hagan algunos cambios en el repositorio, tanto en los archivos que ya existen y creando un nuevo archivo y, una vez terminados, guarden este punto en la historia del desarrollo. ¿Cómo pueden verificar que el branch alicia donde hicieron los cambios tiene al menos un commit más que el branch master?

Ejercicio 6: (a) Cámbiense de rama a master. ¿Qué pasó con los cambios? (b) Vuelvan a cambiarse al branch alicia, y respondan la misma pregunta que antes.


El punto importante hasta el momento es que la historia de los dos branches (locales) ha divergido, y ambas historias están en ambas ramas.

Supongamos ahora que ya están satisfechos con los cambios que han hecho, después de muchas pruebas exhaustivas y otras fallidas (tal vez en otras ramas). Ahora queremos poner estos cambios en la rama master. Para esto, primero nos cambiamos a master, que es la rama a donde queremos pasar los cambios, y después hacemos un merge, o sea, fundimos las dos historias:

    > git checkout master   
    > git merge <nombre_rama>

Ejercicio 7: Pasen los cambios de alicia a master.

Ejercicio 8: Ya que los cambios que hicimos en alicia están en master, borren la rama alicia. Para esto, la instrucción git branch -d alicia es particularmente útil.

Ejercicio 9: Usen LearnGitBranching para jugar con esto y ver gráficamente qué significan las ramas y la divergencia de las historias. En esta misma liga hay otros tutoriales que pueden ser interesantes.


3. Resumen: Flujo colaborativo con git y GitHub

3.1 Iniciar un proyecto

  • Crear un repositorio local de git en tu máquina
  • Crear un repositorio en GitHub
  • Empujar/subir los cambios de tu repositorio local a tu repositorio en GitHub

Nota: Vale la pena incluir un archivo LICENSE.md, donde definen la manera en que uno puede usar el contenido de su proyecto. Para código, se recomienda la licencia MIT.

3.2 Contribuir a otro proyecto en GitHub

  • Desde la página (en GitHub) de algún proyecto al que quieras contribuir, aprieta el botón "Fork". Esto creará una copia del repositorio ajeno en tu cuenta de GitHub.
  • Clona este repositorio (el que está en tu cuenta de GitHub) a tu máquina.
  • Si haces cambios al proyecto (en tu repositorio local, desde una rama distinta a master, y quieres que éstos se evalúen para usarse en el proyecto colaborativo central, debes primero actualizar tu fork (en GitHub) con tus cambios. Para esto, subirás tus cambios a tu repositorio en GitHub usando git push origin <mi_rama>, donde <mi_rama> es la rama donde hiciste los cambios que quieres subir.
  • Ahora puedes hacer una petición para subir tus cambios (pull request) al proyecto central, directamente desde la página de GitHub de tu versión del repositorio, o usando el comando hub pull-request en la línea de comandos si tienes instalado hub (una versión extendida de git que funciona específicamente con GitHub).

Ejercicio 10: Ensaya lo anterior con el repositorio creado por otro compañero en GitHub. Ciertamente todo esto por ahora es de prueba, pero será vital de ahora en adelante :-)


4. Facilitar el flujo de trabajo con GitHub

Para evitar que GitHub te esté pidiendo tu usuario y contraseña todo el tiempo, es necesario usar claves de SSH (SSH keys). En Linux y Mac, el procedimiento es como sigue. NB: No hacer esto desde una máquina/cuenta pública.

Utiliza el comando ssh-keygen para generar claves nuevas. Te pedirá que pongas una clave ("passphrase"); esta clave tendrás que ponerla sólo una vez por sesión.


In [ ]:
;ssh-keygen

Esto generará claves en el directorio escondido ~/.ssh en tu directorio hogar:


In [1]:
;ls ~/.ssh


github_rsa
github_rsa.pub
id_rsa
id_rsa.pub
known_hosts

Ahora, copia la clave pública; esto se puede hacer a mano (copiando el contenido del archivo id_rsa.pub), o usando un programa. E.g. en Mac, puedes usar pbcopy para copiar el contenido de un archivo al clipboard:


In [2]:
;pbcopy < ~/.ssh/id_rsa.pub

Ahora, hay que dar de alta las claves en GitHub:

  • Ve a la página de tu cuenta en GitHub
  • Escoge Settings (arriba, del lado derecho)
  • Escoge SSH keys
  • Escoge Add SSH key
  • Pega lo que copiaste

Ya deberías poder hacer transacciones con GitHub sin que te pida tu usuario cada vez.

Una vez más, no hagas esto en un máquina o cuenta pública.

5. Trabajar con un fork

Normalmente hacemos un fork de un repositorio de interés en GitHub, es decir, una copia del repositorio en tu propia cuenta de GitHub.

Al hacer git clone ... de tu fork, git provee un remote (es decir, un nombre para un repositorio remoto) llamado origin, que apunta a tu fork. Esto lo podemos ver con


In [5]:
; git remote -v


origin	https://github.com/dpsanders/MetodosNumericosAvanzados.git (fetch)
origin	https://github.com/dpsanders/MetodosNumericosAvanzados.git (push)

Vemos que origin apunta al fork del repositorio MetodosNumericosAvanzados en mi cuenta de GitHub (con usuario dpsanders).

Sin embargo, para mantener actualizado nuestro fork con respecto al repositorio original, debemos darle a conocer a git que también existe dicho repositorio. Si hacemos


In [6]:
;git help remote


GIT-REMOTE(1)                     Git Manual                     GIT-REMOTE(1)



NNAAMMEE
       git-remote - Manage set of tracked repositories

SSYYNNOOPPSSIISS
       _g_i_t _r_e_m_o_t_e [-v | --verbose]
       _g_i_t _r_e_m_o_t_e _a_d_d [-t <branch>] [-m <master>] [-f] [--[no-]tags] [--mirror=<fetch|push>] <name> <url>
       _g_i_t _r_e_m_o_t_e _r_e_n_a_m_e <old> <new>
       _g_i_t _r_e_m_o_t_e _r_e_m_o_v_e <name>
       _g_i_t _r_e_m_o_t_e _s_e_t_-_h_e_a_d <name> (-a | --auto | -d | --delete | <branch>)
       _g_i_t _r_e_m_o_t_e _s_e_t_-_b_r_a_n_c_h_e_s [--add] <name> <branch>...
       _g_i_t _r_e_m_o_t_e _s_e_t_-_u_r_l [--push] <name> <newurl> [<oldurl>]
       _g_i_t _r_e_m_o_t_e _s_e_t_-_u_r_l _-_-_a_d_d [--push] <name> <newurl>
       _g_i_t _r_e_m_o_t_e _s_e_t_-_u_r_l _-_-_d_e_l_e_t_e [--push] <name> <url>
       _g_i_t _r_e_m_o_t_e [-v | --verbose] _s_h_o_w [-n] <name>...
       _g_i_t _r_e_m_o_t_e _p_r_u_n_e [-n | --dry-run] <name>...
       _g_i_t _r_e_m_o_t_e [-v | --verbose] _u_p_d_a_t_e [-p | --prune] [(<group> | <remote>)...]


DDEESSCCRRIIPPTTIIOONN
       Manage the set of repositories ("remotes") whose branches you track.

OOPPTTIIOONNSS
       -v, --verbose
           Be a little more verbose and show remote url after name. NOTE: This
           must be placed between remote and subcommand.

CCOOMMMMAANNDDSS
       With no arguments, shows a list of existing remotes. Several
       subcommands are available to perform operations on the remotes.

       _a_d_d
           Adds a remote named <name> for the repository at <url>. The command
           git fetch <name> can then be used to create and update
           remote-tracking branches <name>/<branch>.

           With -f option, git fetch <name> is run immediately after the
           remote information is set up.

           With --tags option, git fetch <name> imports every tag from the
           remote repository.

           With --no-tags option, git fetch <name> does not import tags from
           the remote repository.

           With -t <branch> option, instead of the default glob refspec for
           the remote to track all branches under the refs/remotes/<name>/
           namespace, a refspec to track only <branch> is created. You can
           give more than one -t <branch> to track multiple branches without
           grabbing all branches.

           With -m <master> option, a symbolic-ref refs/remotes/<name>/HEAD is
           set up to point at remote's <master> branch. See also the set-head
           command.

           When a fetch mirror is created with --mirror=fetch, the refs will
           not be stored in the _r_e_f_s_/_r_e_m_o_t_e_s_/ namespace, but rather everything
           in _r_e_f_s_/ on the remote will be directly mirrored into _r_e_f_s_/ in the
           local repository. This option only makes sense in bare
           repositories, because a fetch would overwrite any local commits.

           When a push mirror is created with --mirror=push, then git push
           will always behave as if --mirror was passed.

       _r_e_n_a_m_e
           Rename the remote named <old> to <new>. All remote-tracking
           branches and configuration settings for the remote are updated.

           In case <old> and <new> are the same, and <old> is a file under
           $GIT_DIR/remotes or $GIT_DIR/branches, the remote is converted to
           the configuration file format.

       _r_e_m_o_v_e, _r_m
           Remove the remote named <name>. All remote-tracking branches and
           configuration settings for the remote are removed.

       _s_e_t_-_h_e_a_d
           Sets or deletes the default branch (i.e. the target of the
           symbolic-ref refs/remotes/<name>/HEAD) for the named remote. Having
           a default branch for a remote is not required, but allows the name
           of the remote to be specified in lieu of a specific branch. For
           example, if the default branch for origin is set to master, then
           origin may be specified wherever you would normally specify
           origin/master.

           With -d or --delete, the symbolic ref refs/remotes/<name>/HEAD is
           deleted.

           With -a or --auto, the remote is queried to determine its HEAD,
           then the symbolic-ref refs/remotes/<name>/HEAD is set to the same
           branch. e.g., if the remote HEAD is pointed at next, "git remote
           set-head origin -a" will set the symbolic-ref
           refs/remotes/origin/HEAD to refs/remotes/origin/next. This will
           only work if refs/remotes/origin/next already exists; if not it
           must be fetched first.

           Use <branch> to set the symbolic-ref refs/remotes/<name>/HEAD
           explicitly. e.g., "git remote set-head origin master" will set the
           symbolic-ref refs/remotes/origin/HEAD to
           refs/remotes/origin/master. This will only work if
           refs/remotes/origin/master already exists; if not it must be
           fetched first.

       _s_e_t_-_b_r_a_n_c_h_e_s
           Changes the list of branches tracked by the named remote. This can
           be used to track a subset of the available remote branches after
           the initial setup for a remote.

           The named branches will be interpreted as if specified with the -t
           option on the _g_i_t _r_e_m_o_t_e _a_d_d command line.

           With --add, instead of replacing the list of currently tracked
           branches, adds to that list.

       _s_e_t_-_u_r_l
           Changes URL remote points to. Sets first URL remote points to
           matching regex <oldurl> (first URL if no <oldurl> is given) to
           <newurl>. If <oldurl> doesn't match any URL, error occurs and
           nothing is changed.

           With _-_-_p_u_s_h, push URLs are manipulated instead of fetch URLs.

           With _-_-_a_d_d, instead of changing some URL, new URL is added.

           With _-_-_d_e_l_e_t_e, instead of changing some URL, all URLs matching
           regex <url> are deleted. Trying to delete all non-push URLs is an
           error.

       _s_h_o_w
           Gives some information about the remote <name>.

           With -n option, the remote heads are not queried first with git
           ls-remote <name>; cached information is used instead.

       _p_r_u_n_e
           Deletes all stale remote-tracking branches under <name>. These
           stale branches have already been removed from the remote repository
           referenced by <name>, but are still locally available in
           "remotes/<name>".

           With --dry-run option, report what branches will be pruned, but do
           not actually prune them.

       _u_p_d_a_t_e
           Fetch updates for a named set of remotes in the repository as
           defined by remotes.<group>. If a named group is not specified on
           the command line, the configuration parameter remotes.default will
           be used; if remotes.default is not defined, all remotes which do
           not have the configuration parameter
           remote.<name>.skipDefaultUpdate set to true will be updated. (See
           ggiitt--ccoonnffiigg(1)).

           With --prune option, prune all the remotes that are updated.

DDIISSCCUUSSSSIIOONN
       The remote configuration is achieved using the remote.origin.url and
       remote.origin.fetch configuration variables. (See ggiitt--ccoonnffiigg(1)).

EEXXAAMMPPLLEESS
       +o   Add a new remote, fetch, and check out a branch from it

               $ git remote
               origin
               $ git branch -r
                 origin/HEAD -> origin/master
                 origin/master
               $ git remote add staging git://git.kernel.org/.../gregkh/staging.git
               $ git remote
               origin
               staging
               $ git fetch staging
               ...
               From git://git.kernel.org/pub/scm/linux/kernel/git/gregkh/staging
                * [new branch]      master     -> staging/master
                * [new branch]      staging-linus -> staging/staging-linus
                * [new branch]      staging-next -> staging/staging-next
               $ git branch -r
                 origin/HEAD -> origin/master
                 origin/master
                 staging/master
                 staging/staging-linus
                 staging/staging-next
               $ git checkout -b staging staging/master
               ...


       +o   Imitate _g_i_t _c_l_o_n_e but track only selected branches

               $ mkdir project.git
               $ cd project.git
               $ git init
               $ git remote add -f -t master -m master origin git://example.com/git.git/
               $ git merge origin


SSEEEE AALLSSOO
       ggiitt--ffeettcchh(1) ggiitt--bbrraanncchh(1) ggiitt--ccoonnffiigg(1)

GGIITT
       Part of the ggiitt(1) suite



Git                               11/26/2014                     GIT-REMOTE(1)

vemos que hay un subcomando add de remote. Así que hacemos


In [7]:
; git remote add upstream https://github.com/lbenet/MetodosNumericosAvanzados.git

El nombre usual que se le asigna al repositorio original es upstream.

Ahora podemos actualizar nuestro repositorio local con


In [8]:
; git pull upstream master


From https://github.com/lbenet/MetodosNumericosAvanzados
 * branch            master     -> FETCH_HEAD
Already up-to-date.
 * [new branch]      master     -> upstream/master

(el cual jala la rama master del repositorio apuntado por upstream).

Ahora al hacer


In [9]:
; git push


To https://github.com/dpsanders/MetodosNumericosAvanzados.git
   24f59f5..ebcb8b0  master -> master

empuja los cambios a origin, o sea, a nuestro propio fork.